﻿/*
 * Base code taken from the Pete Montgomery blog - http://petemontgomery.wordpress.com/2008/08/07/caching-the-results-of-linq-queries/
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;
using SBToolkit.Core;

namespace SBCache
{
    /// <summary>
    /// Defines Cache store for the LINQ query results.
    /// </summary>
    public static class LinqCache
    {
        /// <summary>
        /// List prefix for the query results.
        /// </summary>
        private const string ListPrefix = "list:";

        /// <summary>
        /// Count prefix for the count results.
        /// </summary>
        private const string CountPrefix = "count:";

        /// <summary>
        /// Value prefix for the single result.
        /// </summary>
        private const string ValuePrefix = "value:";

        /// <summary>
        /// Cache to use.
        /// </summary>
        private static ICache cache;

        /// <summary>
        /// Gets current cache.
        /// </summary>
        /// <value>Current cache store.</value>
        public static ICache Current
        {
            get
            {
                return cache;
            }
            private set
            {
                cache = value;
            }
        }

        /// <summary>
        /// Gets or sets cache sliding expiration period.
        /// </summary>
        /// <value>The sliding expiration.</value>
        public static TimeSpan? SlidingExpiration { get; set; }

        /// <summary>
        /// Gets or sets cache absolute expiration.
        /// </summary>
        /// <value>The absolute expiration.</value>
        public static DateTime? AbsoluteExpiration { get; set; }

        /// <summary>
        /// Sets the cache store.
        /// </summary>
        /// <param name="cache">The cache.</param>
        public static void SetCache(ICache cache)
        {
            Current = cache;
        }

        /// <summary>
        /// Returns the result of the query as a list; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static IList<T> AsCacheableList<T>(this IQueryable<T> query)
        {
            return query.AsCacheableList(typeof(T).Name);
        }

        /// <summary>
        /// Returns the result of the query as a list; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <param name="dependency">Cache dependency.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static IList<T> AsCacheableList<T>(this IQueryable<T> query, string dependency)
        {
            return query.AsCacheableList(dependency, null);
        }

        /// <summary>
        /// Returns the result of the query as a list; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <param name="dependency">Cache dependency.</param>
        /// <param name="additionalKeyValue">The additional key value.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static IList<T> AsCacheableList<T>(this IQueryable<T> query, string dependency, string additionalKeyValue)
        {
            string key;

            var result = GetQueryResult(ListPrefix, query, additionalKeyValue, out key) as List<T>;
            if (result == null)
            {
                // materialize the query   
                result = query.ToList();

                PutToCache(key, result, dependency);
            }

            return result;
        }

        /// <summary>
        /// Returns the count of the query items; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static int AsCacheableCount<T>(this IQueryable<T> query)
        {
            return query.AsCacheableCount(typeof(T).Name);
        }

        /// <summary>
        /// Returns the count of the query items; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <param name="dependency">Cache dependency.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static int AsCacheableCount<T>(this IQueryable<T> query, string dependency)
        {
            return query.AsCacheableCount(dependency, null);
        }

        /// <summary>
        /// Returns the count of the query items; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <param name="dependency">Cache dependency.</param>
        /// <param name="additionalKeyValue">The additional key value.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static int AsCacheableCount<T>(this IQueryable<T> query, string dependency, string additionalKeyValue)
        {
            string key;
            int count;

            var result = GetQueryResult(CountPrefix, query, additionalKeyValue, out key);
            if (result == null || !Int32.TryParse(result.ToString(), out count))
            {
                // materialize the query   
                count = query.Count();

                PutToCache(key, count, dependency);
            }

            return count;
        }

        /// <summary>
        /// Returns the value of the query item; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static T AsCacheableValue<T>(this IQueryable<T> query)
        {
            return query.AsCacheableValue(null);
        }

        /// <summary>
        /// Returns the value of the query item; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <param name="dependency">Cache dependency.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static T AsCacheableValue<T>(this IQueryable<T> query, string dependency)
        {
            return query.AsCacheableValue(dependency, null);
        }

        /// <summary>
        /// Returns the value of the query item; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <typeparam name="T">Type of the result of the query to return.</typeparam>
        /// <param name="query">The query.</param>
        /// <param name="dependency">Cache dependency.</param>
        /// <param name="additionalKeyValue">The additional key value.</param>
        /// <returns>Result of the query from the cache.</returns>
        public static T AsCacheableValue<T>(this IQueryable<T> query, string dependency, string additionalKeyValue)
        {
            string key;
            var value = default(T);

            var result = GetQueryResult(ValuePrefix, query, additionalKeyValue, out key);
            if (result == null || result.GetType() != typeof(T))
            {
                // materialize the query   
                value = query.FirstOrDefault();

                PutToCache(key, value, dependency);
            }

            return value;
        }

        /// <summary>
        /// Returns query items; if possible from the cache, otherwise
        /// the query is materialized and the result cached before being returned.
        /// </summary>
        /// <param name="cachePrefix">The cache prefix.</param>
        /// <param name="query">The query.</param>
        /// <param name="additionalKeyValue">The additional key value.</param>
        /// <param name="key">The key.</param>
        /// <returns>Result of the query from the cache.</returns>
        private static object GetQueryResult(string cachePrefix, IQueryable query, string additionalKeyValue, out string key)
        {
            if (cache == null)
            {
                throw new Exception("Cache store is not specified.");
            }

            // use the string representation of the query for the cache key
            // locally evaluate as much of the query as possible
            key = Evaluator.PartialEval(query.Expression, CanBeEvaluatedLocally()) + additionalKeyValue ?? String.Empty;

            // the key is potentially very long, so use an md5 fingerprint   
            // (fine if the query result data isn't critically sensitive)   
            key = (cachePrefix + key).ComputeHash();

            // try to get the query result from the cache   
            return cache.Get(key);
        }

        /// <summary>
        /// Puts results to the cache with specified key and, optionally, dependencies.
        /// </summary>
        /// <param name="key">The cache key.</param>
        /// <param name="result">The query result.</param>
        /// <param name="dependency">The cache dependency.</param>
        private static void PutToCache(string key, object result, string dependency)
        {
            if (String.IsNullOrEmpty(dependency))
            {
                cache.Put(key, result, AbsoluteExpiration ?? SystemDateTime.Now().AddMinutes(1));
            }
            else
            {
                cache.Put(key, result, dependency, SlidingExpiration ?? TimeSpan.FromMinutes(10));
            }
        }

        /// <summary>
        /// Determines whether current expression can be evaluated locally.
        /// </summary>
        /// <returns><c>True</c> if can be evaluated locally; otherwise, <c>false</c>.</returns>
        private static Func<Expression, bool> CanBeEvaluatedLocally()
        {
            return expression =>
            {
                // don't evaluate parameters   
                if (expression.NodeType == ExpressionType.Parameter)
                {
                    return false;
                }

                // can't evaluate queries   
                if (typeof(IQueryable).IsAssignableFrom(expression.Type))
                {
                    return false;
                }

                return true;
            };
        }
    }
}
